package com.hooper.dp.service.impl;

import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hooper.dp.dto.RedisData;
import com.hooper.dp.entity.Shop;
import com.hooper.dp.exception.BusinessException;
import com.hooper.dp.mapper.ShopMapper;
import com.hooper.dp.service.IShopService;
import com.hooper.dp.utils.RedisClient;
import com.hooper.dp.utils.RedisLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.*;

import static com.hooper.dp.constant.CommonConstant.COMMON_TTL;
import static com.hooper.dp.constant.ShopConstant.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedisClient redisClient;

    private static final ExecutorService CACHE_SHOP_EXECUTOR =
            Executors.newFixedThreadPool(10);

    /**
     * 互斥锁版本解决缓存击穿
     *
     * @param id 商铺id
     * @return
     */
    @Override
    public Shop getShopById(Long id) {
        //互斥锁方式解决缓存击穿（高数据一致性）
        //return this.getShopWithSyncLock(id);
        //逻辑过期方式解决缓存击穿（低数据一致性）
        //return this.getShopByIdWithLogicExpire(id);

        //使用工具类解决缓存穿透问题
        //return redisClient.getPreventCachePassThrough(SHOP_CACHE_KEY, id, Shop.class, this::getById);
        //使用工具类 解决缓存击穿问题（逻辑过期方式）
        return redisClient.getLogicalExpireData(SHOP_CACHE_KEY, id, Shop.class, this::getById);
    }

    private Shop getShopWithSyncLock(Long id) {
        String shopCacheKey = SHOP_CACHE_KEY + id;
        //从redis中获取缓存数据
        String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
        //若redis中不含该商铺的缓存，则查询数据库并缓存
        if (StringUtils.isNotBlank(shopJson)) {
            //将json数据转为bean
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        RedisLockUtil redisLock = new RedisLockUtil(stringRedisTemplate, Long.toString(id));
        try {
            if (!redisLock.tryLock()) {
                Thread.sleep(50);
                getShopById(id);
            }
            //双检,由于其他线程在获取锁的线程释放锁后，代码的执行位置已到此处。
            String json = stringRedisTemplate.opsForValue().get(shopCacheKey);
            if (StringUtils.isNotBlank(json)) {
                return JSONUtil.toBean(json, Shop.class);
            }
            Shop shop = this.getById(id);
            if (shop == null) {
                throw new BusinessException("商铺不存在！");
            }
            stringRedisTemplate.opsForValue().set(shopCacheKey,
                    JSONUtil.toJsonStr(shop), SHOP_CACHE_TTL, TimeUnit.MINUTES);
            return shop;
        } catch (InterruptedException e) {
            throw new BusinessException(e.getMessage());
        } finally {
            redisLock.unlock();
        }
    }

    @PostConstruct
    public void initLogicData() {
        RedisData<Shop> redisData = new RedisData<>();
        redisData.setData(this.getById(1));
        redisData.setExpireTime(LocalDateTime.now());
        stringRedisTemplate.opsForValue().set(SHOP_CACHE_KEY + "1", JSONUtil.toJsonStr(redisData));
        log.info("缓存添加成功！：{}", stringRedisTemplate.opsForValue().get(SHOP_CACHE_KEY + "1"));
    }

    @Deprecated
    public Shop getShopByIdWithLogicExpire(Long id) {
        String shopCacheKey = SHOP_CACHE_KEY + id;
        Map<String, Shop> shopMap = this.getShopFromRedis(shopCacheKey);
        Shop shop = shopMap.get(NEW_SHOP_DATA);
        if (shop != null) {
            return shop;
        }
        //缓存数据
        RedisLockUtil redisLock = new RedisLockUtil(stringRedisTemplate, Long.toString(id));
        try {
            //获取锁
            if (redisLock.tryLock()) {
                //双检
                Map<String, Shop> doubleCheckMap = this.getShopFromRedis(shopCacheKey);
                Shop newShopData = doubleCheckMap.get(NEW_SHOP_DATA);
                if (newShopData != null) {
                    return newShopData;
                }
                //第一个抢到锁的线程另开一个线程进行缓存操作
                CACHE_SHOP_EXECUTOR.submit(() -> {
                    RedisData<Shop> shopRedisData = new RedisData<>();
                    shopRedisData.setData(this.getById(id));
                    shopRedisData.setExpireTime(LocalDateTime.now().plusMinutes(COMMON_TTL));
                    stringRedisTemplate.opsForValue().set(shopCacheKey,
                            JSONUtil.toJsonStr(shopRedisData));
                });
            }
        } finally {
            redisLock.unlock();
        }
        //直接返回旧数据
        return shopMap.get(OLD_SHOP_DATA);
    }

    @Deprecated
    private Map<String, Shop> getShopFromRedis(String shopCacheKey) {
        Map<String, Shop> shopMap = new ConcurrentHashMap<>(1);
        //从redis中获取缓存数据
        String shopJson = stringRedisTemplate.opsForValue().get(shopCacheKey);
        //参数校验,由于设置的逻辑过期，故不可能为空。
        RedisData<Shop> redisData = JSONUtil.toBean(shopJson,
                new TypeReference<RedisData<Shop>>() {
                }.getType(), false);
        Shop shop = redisData.getData();
        //若逻辑时间未过期直接返回
        LocalDateTime expireTime = redisData.getExpireTime();
        LocalDateTime now = LocalDateTime.now();
        shopMap.put(now.isBefore(expireTime) ? NEW_SHOP_DATA : OLD_SHOP_DATA, shop);
        return shopMap;
    }

    @Override
    public void updateShopById(Shop shop) {
        if (shop == null) {
            throw new BusinessException("the shop param is null!");
        }
        this.updateById(shop);
        //更新数据后删除缓存
        stringRedisTemplate.delete(SHOP_CACHE_KEY + shop.getId());
    }
}
